iT邦幫忙

2022 iThome 鐵人賽

DAY 9
0
自我挑戰組

向網頁施點魔法粉 framer-motion 系列 第 9

#09 Bye Bye Bye ! AnimatePresence Component

  • 分享至 

  • xImage
  •  

<AnimatePresence> 是使用在元件 unmount 時的離場動畫,在 React 的生命週期裡 unmount 就直接從畫面上消失了,如果要做離場動畫必須東補西補,利用布林值判斷動畫結束與消失的時機,motion API 提供了 <AnimatePresence> 允許元件消失之前做動畫。

目錄

  1. before AnimatePresence ?
  2. 華麗離場 : exit props
  3. 如果又重來 : 取消元件的 initial
  4. 轉吧轉吧 : 旋轉木馬 (Carousel)

本節資源:
程式碼

before AnimatePresence ?

React 缺少了在 unmount 之前能夠做完動畫的生命週期,所以當元件 unmount 就直接消失於畫面中,非常之突然。為了解決這個問題,可以設兩道閘門,一個是確認動畫結束,一個是真正 unmount,把元件從畫面消失。

// CSS
.outsider{
    width: 100px;
    height: 100px;
    background: #fa0;
    line-height: 100px;
    text-align: center;
    cursor: pointer;
}

// 當動畫啟動追加上去
.outsider[data-hidding="true"]{
   opacity: 0;
   transition: all 1s ease-out;
}

// JS
import React, { useState } from "react";
import "./style.css";

export default function ComponentFadeOut() {
    const [hidding, setHidding] = useState(false); // 動畫確認開始
    const [hidden, setHidden] = useState(false); // 動畫結束做 unmount

    return (
        <>
            {!hidden && (
                <div
                    className="outsider"
                    {/* 動畫的啟動 */}
                    data-hidding={hidding}
                    onClick={() => {
                        setHidding(true);
                        // 隔 1.5 秒消失
                        setTimeout(() => {
                            setHidden(true);
                        }, 1500);
                    }}
                >
                    邊緣人
                </div>
            )}
        </>
    );
}

在 motion API 可以使用 <AnimatePresence> 解決這個問題,讓真正 unmount 的時間延遲 (defer),讓畫面做完動畫。

import { AnimatePresence, motion } from "framer-motion";
import React, { useState } from "react";
import "./style.css";

export default function AnimatePresenceComponent() {
    // 透過 hidden 判斷要不要消失就好
    const [hidden, setHidden] = useState(false);

    return (
        <>
            <h3>AnimatePresence</h3>
            {/* 加入 AnimatePresence */}
            <AnimatePresence>
                {!hidden && (
                    <motion.div
                        className="outsider"
                        onClick={(e) => {
                            setHidden(true);
                        }}
                        transition={{
                          duration: 1.5
                        }}
                        {/* exit 離場動畫編排 */}
                        exit={{
                            opacity: 0,
                        }}
                    >
                        邊緣人 2 號
                    </motion.div>
                )}
            </AnimatePresence>
        </>
    );
}
  • 效果

AnimatePresence 主要目的有兩個 :

  1. 通知元件什麼時候要消失
  2. 延遲 (defer) unmount 等到完成動畫操作再執行

華麗離場 : exit props

當 motion 元件要消失在渲染樹中要搭配 exit props,會在 DOM 將節點移除之前觸發動畫 :

import { motion, AnimatePresence } from "framer-motion"

export const MyComponent = ({ isVisible }) => (
  // 將會移除 render tree 的 motion 元件用 AnimatePresence 包裹起來
  <AnimatePresence>
    {isVisible && (
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }} // 添加 exit props
      />
    )}
  </AnimatePresence>
)

<AnimatePresence> 會偵測子元件會從渲染樹移除,不過要注意的是子元素最外層 必須為加上唯一的 key,讓 React 知道哪一個要操作,跟一般陣列渲染一樣,如果有 刪除/新增/交換順序 一定要加上唯一的 key,而且避免使用 index 發生非預期的事情

另外 exit 也跟其他基本的 props (initial 、 animate) 一樣,可以透過 variants 使用字串標籤,也可以個別設定 transition 數值。

如果又重來 : 取消元件的 initial

motion 元件在 mount 之後會執行 initialanimate 的動畫,如果 initial = false 其初始狀態就是從 animate 開始。 <AnimatePresence> 的操作是把 key 換掉,也就是又重新出現的 motion 元件依然會從 initial 開始,如果只想要在第一次 render 取消 initial 怎麼辦 ?

AnimatePresence 上加入 initial=false

<AnimatePresence initial={false}>
  ...
</AnimatePresence >

簡單來說,在第一次跳過 initial 動畫。

轉吧轉吧 : 旋轉木馬 (Carousel)

W3shcool 範例 : How To Create a Slideshow

  • 完成效果 :

  • 主要離場動畫的部分

// 動畫 variants 的部份
const imageVariants = {
    enter: (dir) => ({
        x: 500 * -dir,  // 如果是往右 (+),下一張進入時要反方向
        opacity: 0,
    }),
    center: {
     /* 由於每一張圖片都是使用 position 都疊在一起,
      要改變 zindex 避免如果有點擊互動點不到元素 */
        zIndex: 1,
        opacity: 1,
        x: 0,
        transition: {
         // 切開 initial 跟 animate 的間隔,下一篇會提到用 "mode" 來改善
           delay: 0.5, 
        },
    },
    exit: (dir) => {
        return {
            zIndex: 0,
            x: 500 * dir,
            opacity: 0,
        };
    },
};

// JS
const [[dir, picNumber], setPic] = useState([0, 0]); // [方向,第幾張]

// 取消第一次子元素的 initial 與傳入 custom 使子元件即使消失能抓到值
<AnimatePresence custom={dir} initial={false}>
    // 記得要有 key !
    <div className="imageBox" key={picNumber}>
        <motion.img
            variants={imageVariants}
            initial={"enter"}
            animate={"center"}
            exit={"exit"}
            custom={dir}
            src={carouselPic[picNumber]}
            alt={picNumber}
        />
    </div>
</AnimatePresence>

底下的小球列表是用到前面文章提到的 layoutIdLayoutGroup,主要拿來共享元件與區域分組用。

<LayoutGroup>
    <div className="dot-group">
        {carouselPic.map((_, index) => (
            <motion.span
                className="dot"
                key={index}
                onClick={() => {
                    if (picNumber === index) return;
                    // 判斷下一個數字與方向
                    setPic([picNumber > index ? -1 : 1, index]);
                }}
                {picNumber === index && (
                    <motion.span
                        layout
                        layoutId="dot-indicator"
                        className="dot-indicator"
                        variants={indicatorVariants}
                        animate="trigger"
                    />
                )}
            </motion.span>
        ))}
    </div>
</LayoutGroup>

<AnimatePresence> 加上 initial = false 是因為不用在一進入頁面就看到活動的效果,在上面 Gif 可以看到初始化後一開始就呈現 animate 動畫了,如果沒有關掉就會從 initial 的 opacity : 0 (因為 dir 初始是 0 不影響) 開始。

另外要注意的是,在 <AnimatePresence> 如果沒有補上 custom 會抓不到值,因為當 motion 元件消失其伴隨的 custom 也會消失,避免在 exit 執行時抓不到動態值,可以在 <AnimatePresence> 補上去。

Too Fast to Handle.

目前已知 Bug ,只要互動超級無敵快,它就會 Bang 不見,在這個例子中按太快那一張圖片就不會顯示,但是再點一下就會回來了,所以不要太手癢 XD。

可以看到它的 initial 到 animate 卡住了,只剩 exit。

總結

  • AnimatePresence : 處理 motion 元件離場動畫,直到完整跑完才執行 unmount。
  • exit : 設定離場的動畫序列,跟其他的 initial 與 animate 一樣,也可以利用 variants 字串標籤,或者細節設定 transition。

AnimatePresence 的概念還有一些,是有關 Custom Component 使用上的問題。明天也會加上 react-router 實作 Page Transition 。關於範例的部分我假日會努力弄完的Q

參考資料

  1. 官方文件 : AnimatePresence | Framer for Developers

上一篇
#08 Everything in ... transition type
下一篇
#10 Ok,Bye... AnimatePresence Page Transition
系列文
向網頁施點魔法粉 framer-motion 15
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言